﻿/*
 * Created by SharpDevelop.
 * User: matsu
 * Date: 5/20/2010
 * Time: 10:19 PM
 * 
 * Copyright (c) 2010 Matthew MacGregor
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
using System;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using System.Diagnostics;
using System.Configuration;
using System.Net;
using System.Net.Sockets;

using Feedback;

//TODO: implement logger.

namespace webui
{
	/// <summary>
	/// Description of MainForm.
	/// </summary>
	public partial class MainForm : Form
	{
		public MainForm()
		{
			
			//
			// The InitializeComponent() call is required for Windows Forms designer support.
			//
			InitializeComponent();

			//Create a debug console.
			this.DebugConsole = new DebugConsole();
			this.DebugConsole.Text = "Debug";
			
			// Loads important configuration settings from the app.config file.
			// The best practice is to always use the object properties to access
			//  them.
			LoadConfigurationSettings();
			LoadDynamicLibraries();
			
		}
		
		//Start the server process when the main window loads.
		private void MainFormLoad(object sender, EventArgs e)
		{
		    try
		    {
			    StartWebServer();
			    AnimateProgressBar( true );
			    
		    }
		    catch ( PortUnavailableException )
		    {
		        MessageBox.Show( "We are unable to find an open port to start " +
		                        "your personal web server.  \nYou may need to " +
		                        "adjust settings in your webui.exe.Config file." );
		        
		        this.Close();
		    }
			

		}
		
		private void AnimateProgressBar( bool animate )
		{
		    if ( animate == true )
		    {
		        
		        progressBar.MarqueeAnimationSpeed = 85;
			    progressBar.Style = ProgressBarStyle.Marquee;
		    }
		    else
		    {
		        progressBar.Style = ProgressBarStyle.Continuous;
		    }
		}
		
		void MainFormShown(object sender, EventArgs e)
		{
		    if( EnableDebugConsole == true ) 
		    {
			    this.DebugConsole.Show();
		    }
			
		}
		
		/*Loads any dynamic libraries that are needed by the application.*/
		private void LoadDynamicLibraries()
		{
//		    try
//		    {
//		        Assembly.Load( "fdbk.dll" );
//		        this.DebugConsoleEnabled = true;
//		    }
//		    catch ( IOException ioe )
//		    {
//		        this.DebugConsoleEnabled = false;
//		        MessageBox.Show(ioe.Message);
//		    }
		}
		
		//Loads any configuration settings for the application
		private void LoadConfigurationSettings()
		{
		    bool debug_console_shown;
		    if ( bool.TryParse( LoadSetting( "EnableDebugConsole" ) , 
		                      out debug_console_shown ) )
		    {
		        this.EnableDebugConsole = debug_console_shown;                
		    }
		    
		    //Load if IncrementPort is true or false, but check to see that the
		    //app.config data is valid:
		    bool is_inc_port;
		    if ( bool.TryParse( LoadSetting( "IncrementPort" ), out is_inc_port ) )
		    {
		        this.IncrementPort = is_inc_port;
		    }
		    else
		    {
		        DebugConsole.PrintLine( String.Format( "IncrementPort setting {0} is invalid.", is_inc_port) );
		        DebugConsole.PrintLine( "Using a default value." );
		    }
		    
		    /*Load the number of times to try incrementing the port number.  If
		    /*successful, set the property.*/
		    int inc_port_tries;
		    if ( int.TryParse( LoadSetting( "IncrementPortTries" ), out inc_port_tries ) )
		    {
		        this.IncrementPortTries = inc_port_tries;
		    }
		    else
		    {
		        DebugConsole.PrintLine( String.Format( "IncrementPortTries setting {0} is invalid.", 
		                               inc_port_tries ) );
		        DebugConsole.PrintLine( "Using a default value." );
		    }
			
		    /*Not much can really go wrong loading the browser title, so there
		     *isn't much error checking to do. */
		    this.Text = LoadSetting("BrowserTitle");
			
			/*Load the port number from the settings.  Check to make sure that
			*it is a valid number.  Also check if a valid port number has been 
		    *selected -- in this case, cinch server is limited to ports >= 1024.
			* */ 
			int int_port;
			string str_port = LoadSetting( "PortNumber" );
			if ( int.TryParse( str_port , out int_port ) )
			{
			    if ( int_port >= 1024 && int_port <= 65535  )
			    {
			        this.Port = str_port;
			    }
			    else
			    {
			        DebugConsole.PrintLine( String.Format( "{0} is outside of the range" +
			                               " 1024 to 65535 and invalid. Using a " +
			                               "default value" , str_port)  );
			    }
			}
			
			/*If the path is rooted ( i.e. has a drive specified ) then we know
			 * it can't be relative. */
			string str_relative_root = LoadSetting("RelativeRoot");
			if ( ! Path.IsPathRooted(str_relative_root) == true )
            {
                //Path is relative
                this.RelativeRoot = LoadSetting("RelativeRoot");
            }
			else
			{
			    DebugConsole.PrintLine(  String.Format( "{0} is not a valid " +
			        "relative path because it begins with a drive letter. " +
			        "Using a default value.", this.RelativeRoot )  );
			}
			
			/*The HomeUrl from the app.config file has this format: 
			 * 'http://localhost:{0}'
			 * */
		    this.HomeUrl = LoadSetting( "HomeUrl" );
		    DebugConsole.PrintLine( String.Format("HomeUrl is : {0} " ,this.HomeUrl) );
		    
		    /* Get the path of to the php executable.  If the path is empty, 
		     * use the default in the folder with the webui executable.  If the
		     * path is not empty, it will be checked for validity, and failing
		     * that test, the default will be used.
		     * */
		    string php_exe_path = LoadSetting( "PhpExePath" );
		    
		    if ( php_exe_path != "" )
		    {
		    	if ( Directory.Exists( php_exe_path ) )
		    	{
	
		    		this.PhpExePath = php_exe_path;
		    	}
		    	else
		    	{
		    	    DebugConsole.PrintLine( "PhpExePath is not valid because " +
		    	                           "it does not exist." );
		    	    DebugConsole.PrintLine( String.Format("PhpExePath: {0}",php_exe_path) );
		    	    DebugConsole.PrintLine( "Using a default value." );
		    	}
		    }
		    
		    /* Load the name of the php executable that will be launched.
		     * Use the default if the name is empty.
		     **/
		    string php_exe_name = LoadSetting( "PhpExeName" );
            //TODO: better testing for a valid executable here.
		    if ( php_exe_name != "" )
		    {
		    	this.PhpExeName = php_exe_name;
		    }
		    else
		    {
		        DebugConsole.PrintLine( "PhpExeName is not valid.  Using default." );
		    }
		}
		
		//Loads an application setting from the configuration file.
		private string LoadSetting(string name )
		{
			string setting = ConfigurationManager.AppSettings[name];
			if( setting == "" )
			{
				DebugConsole.PrintLine(
			    String.Format("Warning: Empty configuration string in LoadSetting({0})", name)
			        );
			}
			return setting;
		}
		
		//Updates a setting and writes it to the file.
		private void UpdateSetting( string key, string val )
		{
			Configuration config = GetConfigObject();
			
            config.AppSettings.Settings[key].Value = val;
            SaveSettings( config );
		}
		
		//Saves the current settings to file
		private void SaveSettings( Configuration config )
		{
			
            config.Save(ConfigurationSaveMode.Modified);
            ConfigurationManager.RefreshSection("appSettings");
            
		}
		
		/*Returns a configuration object, used for updating application
		 *settings.*/
		private Configuration GetConfigObject()
		{
			return
				ConfigurationManager.OpenExeConfiguration(
					ConfigurationUserLevel.None);
		}
		
		/*Check if the port indicated is available to use.  True if it is open,
		 *false if it is being used. */
		private bool IsPortAvailable( int port )
		{
            IPAddress ipAddress = Dns.GetHostEntry("localhost").AddressList[0];
            try {
                TcpListener tcpListener = new TcpListener(ipAddress, port);
                tcpListener.Start();
                tcpListener.Stop();
                //Something must already be serving...
                return true;
              }
              catch (SocketException ) {
                //Okay, something is using this port...
                return false;
              }
		}
		
		/**
		 * The HomeUrl from the app.config file has this format: 
		 * 		'http://localhost:{0}' 
		 * Format the url with the appropriate port number, or failing that, 
		 * use the default.
		 * */
		private string FormatHomeUrl( string port )
		{
			/*
			 * We need to format it before using it.*/
		    string homeurl = LoadSetting( "HomeUrl" );
		    try
		    {
			    homeurl = System.String.Format( homeurl, port );
		    } 
		    //If there is a format problem, just use the default setting.
		    catch( FormatException ) 
		    {
		        homeurl = System.String.Format( _homeUrl, this.Port );
		    }
		    
		    return homeurl;
		}
		
		/*Look for an open port, starting at 'port' and incrementing 'tries'
		 *  times.*/
		private string FindOpenPort( string port, int tries )
		{
		    bool success = false;
		    //Convert the port ( string ) to an int for easy processing.
		    int int_port = Int32.Parse( this.Port );
		    if ( this.IncrementPort )
		    {
			    for ( int i = int_port ; i < (int_port + tries); i++ )
			    {
			        
			        if ( IsPortAvailable( i ) )
	    	        {
	    		        //Found an open port, return it as a string
	    		        port = i.ToString();
	    		        success = true;
                        DebugConsole.PrintLine(String.Format("Port: {0} ", port));
	    		        break;
	    	        }
			    }
		    }
		    
		    if ( ! success )
		    {
		        throw new PortUnavailableException("No open port available in "+
		              "range of " + port + " to " + int_port + "." );
		    }
		    
		    return port;
		}
		
		/*Checks if a custom path to the cgi executable has been set, returns
		 * the result or the default. */
		private string GetPhpExecutablePath( bool quotePath )
		{	
			string path = "";
			//must have set a custom path
			if ( this.PhpExePath.Length > 0 )
			{
				path = Path.Combine( this.PhpExePath , this.PhpExeName );
				//If the path has spaces, wrap the path in quotes.
				if ( path.Contains(" " ) && quotePath == true )
				{
				    path = String.Format("\"{0}\"" , path );
				}
			}

			return path;
		}
			
		public void StartWebServer()
		{
		    
    		//Try to find an open port, throws an exception if not available:
    		this.Port = FindOpenPort( this.Port, this.IncrementPortTries );

			 // Use ProcessStartInfo class
        	ProcessStartInfo startInfo          = new ProcessStartInfo();
        	startInfo.CreateNoWindow            = true;
        	startInfo.UseShellExecute           = false;
        	startInfo.RedirectStandardOutput    = true;
        	startInfo.RedirectStandardError     = true;
        	startInfo.FileName = LoadSetting("LaunchCommand");
        	//startInfo.WindowStyle             = ProcessWindowStyle.Hidden;
        	startInfo.Arguments                 = this.Port + " " 
        						                + this.RelativeRoot + " " 
        	                                    //Get path to php and wrap in ""
        						                + GetPhpExecutablePath( true );

        	//MessageBox.Show( startInfo.Arguments );

	        try
	        {
	            
	            //Launch the web server in a new process.
	            this.Webserver = Process.Start( startInfo );
                DebugConsole.PrintLine( this.HomeUrl ); 
                this.webBrowser.Navigate( this.HomeUrl );
	            
	            
	        }
	        catch ( Exception e )
	        {
	            // Log error.
	            MessageBox.Show("Sorry, we were unable to start the web server." );
	            DebugConsole.PrintLine( e.GetType().ToString() + ": " + e.Message);
	            this.Close();
	         
	        }
	        finally
	        {
	        	StartServerChecker();
	        }
		}
		
		//Launches a thread that checks on the status of the server process
		//approximately once a second.  It warns the main process if the server
		//has stopped.
		private bool StartServerChecker()
		{
			//If the check server thread is already alive, don't start it again!
			if( checkServerThread != null && checkServerThread.IsAlive ) 
			{ 
				return true; 
			}
			//success is true only in an exception isn't thrown while starting thread.
			bool success = false;
			try 
			{
				
				//Create a thread that checks the server once / second to
	            //make sure that it updates the ui if the server quits.
	            ThreadStart job = new ThreadStart(CheckServer);
        		this.checkServerThread = new Thread(job);
        		this.checkServerThread.Start();
        		success = true;
			}
			catch
			{
				success = false;
			}
			return success;
		}
		
		//When the main form is closing, shutdown threads and processes.
		void MainFormFormClosing(object sender, FormClosingEventArgs e)
		{
			try
			{
				//Turn off the thread that checks the server:
				if ( this.checkServerThread.IsAlive == true ) {
					IsCheckingServer = false;
				}
				//We need to wait for the checkServerThread...
				Thread.Sleep( 1100 );
				//Wait for the browser to finish loading a page:
				int i = 0;
				while( webBrowser.IsBusy )
				{
					//wait for a while, then shut it down...
					if ( i == 15 ) break;
					Thread.Sleep( 800 );
					i++;
				}
				
				DebugConsole.Close();
				//Shut down the web server.
				Webserver.Kill();
				Webserver.WaitForExit();
				
			} 
			catch //in case already killed...
			{
                DebugConsole.PrintLine("There was an error closing the application.");
                DebugConsole.PrintLine("A Thread or Process failed to end correctly.");
                DebugConsole.PrintLine("Logged from webui.MainForm.MainFormFormClosing");
			}
		}
		
		//Check to see if the server is currently running and update the ui.
		private void CheckServer()
		{
			while( true )
			{
				Thread.Sleep(1000);

				if ( Webserver != null && Webserver.HasExited == true  )
				{
					UpdateServerStatus("Off");
					MessageBox.Show("Your personal web server has stopped unexpectedly.");
					//Okay this is temporary... TODO: fix me...
			        System.IO.StreamReader reader = Webserver.StandardOutput; 
	                string sRes = reader.ReadToEnd();
                    reader.Close();    
                    this.DebugConsole.AppendText( sRes + "\n" );
                    this.DebugConsole.AppendText("Webserver has exited: " + Webserver.HasExited);
					break;
				}
				else if (  IsCheckingServer == false )
				{
					break;
				}
				
			}
		}
		
		//Updates the status of the server on the ui for the program.  Toggles
		//from on to off...
		private void UpdateServerStatus(string value)
	    {
	        if (InvokeRequired)
	        {
	            // We're not in the UI thread, so we need to call BeginInvoke
	            BeginInvoke(new StringParameterDelegate(UpdateServerStatus), 
	                        new object[]{value});
	            return;
	        }
	        // Must be on the UI thread if we've got this far
	        this.labelServerStatus.Text = value;
	        this.labelServerStatus.ForeColor = Color.Red;
	    }
		
		private void UpdateUrlBox( )
		{
        
            if (webBrowser.Url != null)
            {

                urlBox.Text = webBrowser.Url.ToString();
            }
            else
            {
                urlBox.Text = "Loading...";
            }
		}
		
		void ButtonRefreshClick(object sender, EventArgs e)
		{
			
			webBrowser.Refresh();
		}
		
		void ButtonBackClick(object sender, EventArgs e)
		{
			webBrowser.GoBack();
		}
		
		void ButtonNextClick(object sender, EventArgs e)
		{
			webBrowser.GoForward();
		}
		
		void ButtonHomeClick(object sender, EventArgs e)
		{
			webBrowser.Navigate( this.HomeUrl );
			//DebugConsole.PrintLine( "Navigating..." );
			//DebugConsole.Show();
		}
		
				
		void WebBrowserNavigating(object sender, WebBrowserNavigatingEventArgs e)
		{
			UpdateUrlBox();
			AnimateProgressBar(true);
			//DebugConsole.PrintLine( "Navigating..." );
		}
		void WebBrowserNavigated(object sender, WebBrowserNavigatedEventArgs e)
		{
			UpdateUrlBox();
			AnimateProgressBar(false);
		
		}
		
	
		
		private delegate void StringParameterDelegate (string value);
		private Thread 			    checkServerThread;
		private Process 		    webserver;
		private volatile bool 	    isCheckingServer 	    = true;
		private string 			    _port 				    = "8080";
		private string 			    _relativeRoot 		    = "www";
		private string 			    _homeUrl 			    = "http://localhost:{0}/";
		private int 			    _incrementPortTries     = 100;
		private bool 			    _incrementPort 		    = true;
		private string			    _phpExePath 		    = "";
		private string 			    _phpExeName 		    = "php-cgi.exe";
		private bool                _debugConsoleEnabled    = false;
		private DebugConsole        _debugConsole;
		private bool                _enableDebugConsole     = false;
		
        public bool EnableDebugConsole {
            get { return _enableDebugConsole; }
            set { _enableDebugConsole = value; }
        }
		
        public DebugConsole DebugConsole {
            get { return _debugConsole; }
            set { _debugConsole = value; }
        }
		
        public bool DebugConsoleEnabled {
            get { return _debugConsoleEnabled; }
            set { _debugConsoleEnabled = value; }
        }
		
		public string PhpExeName {
			get { return _phpExeName; }
			set { _phpExeName = value; }
		}
		
		public string PhpExePath {
			get { return _phpExePath; }
			set { _phpExePath = value; }
		}
		
		
        public int IncrementPortTries {
            get { return _incrementPortTries; }
            set { _incrementPortTries = value; }
        }
		
        public bool IncrementPort {
            get { return _incrementPort; }
            set { _incrementPort = value; }
        }
		
        public string HomeUrl {
		    get { return FormatHomeUrl(this.Port); }
            set { _homeUrl = value; }
        }
		
        public string RelativeRoot {
		    
            get { return _relativeRoot; }
            set { _relativeRoot = value; }
        }
		
        public string Port {
            get { return _port; }
            set { _port = value; }
        }
		
		public bool IsCheckingServer {
			get { return isCheckingServer; }
			set { isCheckingServer = value; }
		}
		public Process Webserver {
			get { return webserver; }
			set { webserver = value; }
		}
	
	}
	
	
}
